package com.listeningframework.boot.commons.httpclient;

import com.google.common.collect.Maps;
import com.listeningframework.boot.commons.http.HttpMethod;
import com.listeningframework.boot.constants.Encoding;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.IOUtils;
import org.apache.http.*;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.config.RequestConfig.Builder;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicHeader;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.Set;

/**
 * 
 * Abstract HttpClient Factory
 *
 * @author lixining
 * @version $Id: AbstractHttpClientFactory.java, v 0.1 2016年8月16日 下午5:42:22 lixining Exp $
 */
public abstract class AbstractHttpClientFactory {
    /**
     * Http Headers
     */
    private static final Map<String, Header> DEFAULT_HEADERS = Maps.newConcurrentMap();
    /**
     * Http Headers
     */
    private static final Map<String, Header> CUSTOM_HEADERS = Maps.newConcurrentMap();
    /**
     * Cookie Store
     */
    private CookieStore cookieStore = new BasicCookieStore();
    /**
     * HttpClient实例
     */
    private final HttpClient httpClient;
    /**
     * Default Encoding
     */
    protected Encoding encoding = Encoding.UTF8;

    public AbstractHttpClientFactory(HttpHost proxy) {
        httpClient = getHttpClientInstance(proxy);
    }

    public AbstractHttpClientFactory() {
        this(null);
    }

    /**
     * Get Instance
     *
     * @return HttpClient
     */
    private HttpClient getHttpClientInstance(HttpHost proxy) {
        DEFAULT_HEADERS.putAll(HttpClientHeaders.getHeaders());

        // Step 1. SSL Security
        SSLConnectionSocketFactory sslsf = null;
        try {
            SSLContext sslContext = SSLContext.getInstance(SSLConnectionSocketFactory.SSL);
            sslContext.init(null, new TrustManager[] { new AnonymousX509TrustManager() }, null);
            sslsf = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            throw new RuntimeException("httpclient sslcontext fail");
        }
        // Step 2. Build HttpClient
        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        httpClientBuilder.setSSLSocketFactory(sslsf);
        httpClientBuilder.setMaxConnPerRoute(40);// 客户端总并行链接最大数
        httpClientBuilder.setMaxConnTotal(400); // 每个主机的最大并行链接数
        httpClientBuilder.setDefaultCookieStore(cookieStore);
        if (proxy != null) {
            httpClientBuilder.setProxy(proxy);
        }
        return httpClientBuilder.build();
    }

    /**
     * Get All Headers With Random User Agent 
     */
    private Header[] getHeaders() {
        addDefaultHeader(HttpHeaders.USER_AGENT, HttpClientHeaders.getUserAgent());
        DEFAULT_HEADERS.putAll(CUSTOM_HEADERS);
        return DEFAULT_HEADERS.values().toArray(new Header[DEFAULT_HEADERS.size()]);
    }

    /**
     * Add Header To Header Set
     *
     * @param name Header名
     * @param value Header值
     */
    private void addDefaultHeader(String name, String value) {
        DEFAULT_HEADERS.put(name, new BasicHeader(name, value));
    }

    /**
     * Add Header To Header Set
     *
     * @param name Header名
     * @param value Header值
     */
    public void addHeader(String name, String value) {
        CUSTOM_HEADERS.put(name, new BasicHeader(name, value));
    }

    /**
     * Get HttpClient Bytes
     *
     * @param clientMethod 调用方法
     * @return byte[]
     * @throws Exception 调用异常
     */
    protected byte[] getHttpClientBytes(HttpClientMethod clientMethod) throws Exception {
        if (clientMethod.isGet()) {
            return handleHttpMethodGet(clientMethod);
        }
        if (clientMethod.isPost()) {
            return handleHttpMethodPost(clientMethod);
        }
        return null;
    }

    /**
     * Handle Http Method Get
     *
     * @param clientMethod 调用方法
     * @return byte[]
     * @throws Exception 调用异常
     */
    private byte[] handleHttpMethodGet(HttpClientMethod clientMethod) throws Exception {
        HttpGet httpGet = new HttpGet(clientMethod.getAddress());
        try {
            httpGet.setHeaders(getHeaders());
            Integer socketTimeout = clientMethod.getSocketTimeout();
            Integer connectTimeout = clientMethod.getConnectTimeout();
            httpGet.setConfig(getCustomConfig(socketTimeout, connectTimeout));
            return execute(clientMethod, httpGet);
        } finally {
            if (httpGet != null) {
                httpGet.releaseConnection();
            }
        }
    }

    /**
     * Handle Http Method Post
     *
     * @param clientMethod 调用方法
     * @throws Exception 调用异常
     */
    private byte[] handleHttpMethodPost(HttpClientMethod clientMethod) throws Exception {
        HttpPost httpPost = new HttpPost(clientMethod.getAddress());
        try {
            HttpEntity httpEntity = clientMethod.getHttpEntity();
            if (httpEntity != null) {
                httpPost.setEntity(httpEntity);
            }
            httpPost.setHeaders(getHeaders());
            Integer socketTimeout = clientMethod.getSocketTimeout();
            Integer connectTimeout = clientMethod.getConnectTimeout();
            httpPost.setConfig(getCustomConfig(socketTimeout, connectTimeout));
            return execute(clientMethod, httpPost);
        } finally {
            if (httpPost != null) {
                httpPost.releaseConnection();
            }
        }
    }

    /**
     * Execute HttpClientMethod
     *
     * @param clientMethod 调用方法
     * @param request 请求
     * @throws Exception 调用异常
     */
    private byte[] execute(HttpClientMethod clientMethod, HttpUriRequest request) throws Exception {
        HttpResponse response = httpClient.execute(request);
        int code = response.getStatusLine().getStatusCode();
        if (code != HttpStatus.SC_OK) {
            throw new RuntimeException("Http Request Access Fail , Return Code(" + code + ")");
        }
        if (HttpClientHeaders.isRedirect(code)) {
            String redirectAddress = response.getFirstHeader("location").getValue();
            clientMethod.setHttpMethod(HttpMethod.GET);
            clientMethod.setAddress(redirectAddress);
            return getHttpClientBytes(clientMethod);
        }
        // Convert Entity To Bytes
        InputStream inputStream = null;
        try {
            HttpEntity httpEntity = response.getEntity();
            if (httpEntity == null || httpEntity.getContent() == null) {
                throw new RuntimeException("Response Entity Is null");
            }
            inputStream = httpEntity.getContent();
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = inputStream.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
            out.flush();
            return out.toByteArray();
        } finally {
            IOUtils.closeQuietly(inputStream);
        }
    }

    /**
     * Construct Request Config
     *
     * @param socketTimeout socket超时
     * @param connectionTimeout 连接超时
     */
    private final RequestConfig getCustomConfig(int socketTimeout, int connectionTimeout) {
        Builder builder = RequestConfig.custom();
        builder.setSocketTimeout(socketTimeout).setConnectTimeout(connectionTimeout);
        return builder.build();
    }

    /**
     * Get Cookie Store
     *
     * @return CookieStore
     */
    public CookieStore getCookieStore() {
        return cookieStore;
    }

    /**
     * Add Cookie To Cookie Store
     *
     * @param cookie Cookie
     */
    public void addCookie(Cookie cookie) {
        this.cookieStore.addCookie(cookie);
    }

    public void addCookies(Set<? extends Cookie> cookies) {
        if (CollectionUtils.isEmpty(cookies)) {
            return;
        }
        for (Cookie cookie : cookies) {
            this.addCookie(cookie);
        }
    }

    /**
     * Setter method for property <tt>encoding</tt>.
     * 
     * @param encoding value to be assigned to property encoding
     */
    public void setEncoding(Encoding encoding) {
        this.encoding = encoding;
    }

    /**
     * Clean All Cookies
     */
    public void cleanCookies() {
        this.cookieStore.clear();
    }

    /**
     * Clean Custom Headers
     */
    public void cleanHeaders() {
        CUSTOM_HEADERS.clear();
    }

}
